#include <iostream>

#include <EGL/egl.h>
#include <GLES3/gl32.h>

#include <wayland-client.h>
#include <wayland-egl.h>

#include <cairo/cairo.h>

#include "ukui-shell.h"
#include "xdg-shell.h"

wl_display *display = nullptr;
wl_registry *registry = nullptr;
wl_compositor *compositor = nullptr;
wl_output *wlOutput = nullptr;
xdg_wm_base *xdgshell = nullptr;
ukui_shell *ukuiShell = nullptr;
wl_surface *surface = nullptr;
wl_region *region = nullptr;
xdg_surface *xdgSurface = nullptr;
ukui_surface *ukuiSurface = nullptr;
EGLDisplay egl_display = nullptr;
wl_egl_window *egl_window = nullptr;
EGLSurface egl_surface = nullptr;
int output_width = 0;
int output_height = 0;

const char *vertexShaderSource = R"(
    #version 300 es
    layout(location = 0) in vec2 position;
    layout(location = 1) in vec2 texCoord;

    out vec2 TexCoord;
    void main() {
        gl_Position = vec4(position, 0.0, 1.0);
        TexCoord = texCoord;
    }
)";

const char *fragmentShaderSource = R"(
    #version 300 es
    precision mediump float;
    out vec4 color;

    uniform sampler2D ourTexture;

    in vec2 TexCoord;
    void main() {
        color = texture(ourTexture, TexCoord);
    }
)";

void resetTexture()
{
    cairo_surface_t *cairo_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, output_width, output_height);
    cairo_t *cr = cairo_create(cairo_surface);
    cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.2);

    const char text[] = "Open Kylin";
    cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size(cr, 256.0);
    cairo_text_extents_t extents;
    cairo_text_extents(cr, text, &extents);

    double x = (output_width - extents.width) / 2 - extents.x_bearing;
    double y = (output_height - extents.height) / 2 - extents.y_bearing;

    cairo_move_to(cr, x, y);
    cairo_show_text(cr, text);

    cairo_destroy(cr);
    cairo_surface_flush(cairo_surface);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, output_width, output_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, cairo_image_surface_get_data(cairo_surface));
    cairo_surface_destroy(cairo_surface);
}

void paintGL()
{
    if (!egl_window || output_height == 0 || output_width == 0) {
        return;
    }
    resetTexture();
    glClearColor(0, 0, 0, 0);
    glClear(GL_COLOR_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    eglSwapBuffers(egl_display, egl_surface);
}

void handle_output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform)
{
    std::cout << "Output Added: " << make << " " << model << " " << physical_width << "x" << physical_height << "(mm)" << std::endl;
}

void handle_output_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh)
{
    std::cout << "Output Mode: " << width << "x" << height << "@" << refresh / 1000 << "Hz" << std::endl;
    output_width = width;
    output_height = height;
    if (egl_window && output_height != 0 && output_width != 0) {
        resetTexture();
        wl_egl_window_resize(egl_window, output_width, output_height, 0, 0);
        glViewport(0, 0, output_width, output_height);
    }
    paintGL();
}

wl_output_listener output_listener = {
    handle_output_geometry,
    handle_output_mode};

void handle_golbal(void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version)
{
    if (std::string(interface) == wl_compositor_interface.name) {
        compositor = static_cast<wl_compositor *>(wl_registry_bind(wl_registry, name, &wl_compositor_interface, 1));
    } else if (std::string(interface) == xdg_wm_base_interface.name) {
        xdgshell = static_cast<xdg_wm_base *>(wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, 1));
    } else if (std::string(interface) == ukui_shell_interface.name) {
        ukuiShell = static_cast<ukui_shell *>(wl_registry_bind(wl_registry, name, &ukui_shell_interface, 1));
    } else if (std::string(interface) == wl_output_interface.name) {
        wlOutput = static_cast<wl_output *>(wl_registry_bind(wl_registry, name, &wl_output_interface, 1));
        wl_output_add_listener(wlOutput, &output_listener, nullptr);
    }
}

void handle_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name)
{
    printf("Got a registry remove event for id %d\n", name);
}

wl_registry_listener registry_listener = {
    handle_golbal,
    handle_global_remove};

void handle_xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
{
    xdg_surface_ack_configure(xdg_surface, serial);
}

xdg_surface_listener surface_listener = {
    handle_xdg_surface_configure};

void handle_xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states)
{
    if (egl_window) {
        resetTexture();
        wl_egl_window_resize(egl_window, output_width, output_height, 0, 0);
        glViewport(0, 0, output_width, output_height);
    }
    paintGL();
}

void handle_xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel)
{
    xdg_surface_destroy(xdgSurface);
    xdg_wm_base_destroy(xdgshell);
    wl_surface_destroy(surface);
    wl_compositor_destroy(compositor);
    wl_registry_destroy(registry);
    wl_display_disconnect(display);
    exit(0);
}

xdg_toplevel_listener toplevel_listener = {
    handle_xdg_toplevel_configure,
    handle_xdg_toplevel_close};

int main(int agrc, char *argv[])
{
    display = wl_display_connect(nullptr);
    registry = wl_display_get_registry(display);
    wl_registry_add_listener(registry, &registry_listener, nullptr);
    wl_display_dispatch(display);
    wl_display_roundtrip(display);

    surface = wl_compositor_create_surface(compositor);
    xdgSurface = xdg_wm_base_get_xdg_surface(xdgshell, surface);
    xdg_toplevel *toplevel = xdg_surface_get_toplevel(xdgSurface);
    xdg_toplevel_set_max_size(toplevel, 66635, 66635);
    xdg_toplevel_set_min_size(toplevel, 100, 100);
    xdg_surface_add_listener(xdgSurface, &surface_listener, nullptr);
    xdg_toplevel_add_listener(toplevel, &toplevel_listener, nullptr);
    // we use ukui shell protocols to make the surface on top, if you compositor is not ukui,
    // you can use other shell protocols to make the surface on top.
    if (ukuiShell) {
        ukuiSurface = ukui_shell_create_surface(ukuiShell, surface);
        ukui_surface_set_role(ukuiSurface, UKUI_SURFACE_ROLE_ONSCREENDISPLAY);
    } else {
        std::cout << "The wayland compositor don't support ukui shell protocols! You need to use other shell protocols to make the watermark surface stay on top." << std::endl;
    }

    region = wl_compositor_create_region(compositor);
    wl_region_add(region, 0, 0, 0, 0);
    wl_surface_set_input_region(surface, region);
    wl_surface_commit(surface);
    wl_display_dispatch(display);

    EGLint config_attribs[] = {
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_NONE};
    EGLint context_attribs[] = {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE};

    EGLConfig egl_config;

    int major, minor, config_count;
    egl_display = eglGetDisplay(EGLNativeDisplayType(display));
    eglInitialize(egl_display, &major, &minor);
    eglGetConfigs(egl_display, nullptr, 0, &config_count);
    eglChooseConfig(egl_display, config_attribs, &egl_config, 1, &config_count);
    EGLContext egl_context = eglCreateContext(egl_display, egl_config, EGL_NO_CONTEXT, context_attribs);

    egl_window = wl_egl_window_create(surface, output_width, output_height);
    egl_surface = eglCreateWindowSurface(egl_display, egl_config, EGLNativeWindowType(egl_window), nullptr);
    eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);

    GLuint VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
// clang-format off
    float vertices[] = {
        -1.0f, -1.0f, 0.0f, 1.0f,
         1.0f, -1.0f, 1.0f, 1.0f,
        -1.0f,  1.0f, 0.0f, 0.0f,
         1.0f,  1.0f, 1.0f, 0.0f
        };
// clang-format on
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float)));
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    GLuint tex;
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, output_width, output_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    glBindVertexArray(VAO);
    glUseProgram(shaderProgram);

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    paintGL();
    while (wl_display_dispatch(display) != -1) {
        paintGL();
    }
    xdg_surface_destroy(xdgSurface);
    xdg_wm_base_destroy(xdgshell);
    wl_surface_destroy(surface);
    wl_compositor_destroy(compositor);
    wl_registry_destroy(registry);
    wl_display_disconnect(display);

    return 0;
}